-------------------------------------------------
-- GcProtoBuf.lua
-------------------------------------------------
-------------------------------------------------
--[[
example:

	local GcProtoBuf = LibStub("GcProtoBuf-1.0")
	pbuf = GcProtoBuf:new()

	pbuf:push(PbType.bool, false, true, 2, 0, nil, 1)
	pbuf:push(PbType.int8, 11, 213)
	pbuf:push(PbType.int32, 1001, 3221, 12331)

	pbuf:push(PbType.uint8, 11, 300)
	pbuf:push(PbType.uint32, 67328, 32213, 731234)

	pbuf:push(PbType.string, "i'm a string", 888888, nil, true)

	pbuf:push(PbType.float, 0, 0.23213, 1332.483943)
	pbuf:push(PbType.double, 0, 0.23213, 1332.483943)

	pbuf:push(PbType.binary, "0011223344", 0, 1000)

	pbuf:list()
--]]
-------------------------------------------------

local MAJOR, MINOR = "GcProtoBuf-1.0", 1
local GcProtoBuf = LibStub:NewLibrary(MAJOR, MINOR)

if not GcProtoBuf then return end

-- Lua APIs
local tconcat, tinsert, tremove = table.concat, table.insert, table.remove
local unpack = unpack
local type, next, pairs, select = type, next, pairs, select
local tonumber, tostring, rawset = tonumber, tostring, rawset
local assert, loadstring, error = assert, loadstring, error
local format, strfind, strsub, strlen = string.format, string.find, string.sub, string.len
local abs, fmod, floor = math.abs, math.fmod, math.floor

-- WoW APIs
local _G = _G

-- type(arg1) = "nil", "boolean", "number", "string", "table", "function", "thread", or "userdata"

PbType = {
	bool	= 1,
	int8	= 2,
	int16	= 3,
	int32	= 4,
	int64	= 5,
	uint8	= 6,
	uint16	= 7,
	uint32	= 8,
	uint64	= 9,
	float	= 10,
	double	= 11,
	string	= 12,
	ustring	= 13,
	binary	= 14,
	array	= 15,
	TYPE_MAX = 16,
}

PbTypeInfo = {
	[PbType.bool]		= { name = "bool",		data_size = 1,	max_val = 0						},
	[PbType.int8]		= { name = "int8",		data_size = 1,	max_val = 128					},
	[PbType.int16]		= { name = "int16",		data_size = 2,	max_val = 32768					},
	[PbType.int32]		= { name = "int32",		data_size = 3,	max_val = 2147483648			},
	[PbType.int64]		= { name = "int64",		data_size = 8,	max_val = 9223372036854775808	},
	[PbType.uint8]		= { name = "uint8",		data_size = 1,	max_val = 256					},
	[PbType.uint16]		= { name = "uint16",	data_size = 2,	max_val = 65536					},
	[PbType.uint32]		= { name = "uint32",	data_size = 4,	max_val = 4294967296			},
	[PbType.uint64]		= { name = "uint64",	data_size = 8,	max_val = 18446744073709551616	},
	[PbType.float]		= { name = "float",		data_size = 4,	max_val = 0						},
	[PbType.double]		= { name = "double",	data_size = 8,	max_val = 0						},
	[PbType.string]		= { name = "string",	data_size = 0,	max_val = 0						},
	[PbType.ustring]	= { name = "ustring",	data_size = 0,	max_val = 0						},
	[PbType.binary]		= { name = "binary",	data_size = 0,	max_val = 0						},
	[PbType.array]		= { name = "array",		data_size = 0,	max_val = 0						},
	[PbType.TYPE_MAX]	= { name = "TYPE_MAX",	data_size = 0,	max_val = 0						},
}

PB_NIL_STRING = "[nil]"
PB_UNKNOWN_STRING = "[unknown]"

GcProtoBuf.core = {
	length = 0,
	data_size = 0,
}

-- byte array
GcProtoBuf._bytes = GcProtoBuf._bytes or {}
-- entry table
GcProtoBuf._entries = GcProtoBuf._entries or {}

GcProtoBuf.debugFrame = ChatFrame1
GcProtoBuf.debugging = false

local function my_iter(t, i)
	i = i + 1
	local v = t[i]
	if v then
		return i, v
	else
		if i <= #t then
			--print("i = "..i..", #t = "..#t)
			return i, nil
		end
	end
end

local function my_ipairs(t)
	return my_iter, t, 0
end

local function select_arg(n, args)
	--print("args = "..args)
	--print_table2(args)
	if n > 0 and n <= #args then
		return args[n]
	else
		return nil
	end
end

local function print_table(t)
	if type(t) ~= "nil" then
		local value = nil
		local type_name = nil
		for i, v in pairs(t) do
			if type(v) == "table" then
				print_table(v)
			else
				if i == "type" then
					--print(i.." = "..tostring(v).." test")
					local typeinfo = PbTypeInfo[v]
					if type(typeinfo) ~= "nil" then
						--print(i.." = "..tostring(typeinfo).." test")
						--print_table(typeinfo)
						type_name = typeinfo.name
						--print("type_name ="..value_name)
					end
					if type(type_name) == "nil" then
						type_name = tostring(v)
					end
					--print(i.." = "..type_name)
				elseif i == "value" then
					value = tostring(v)
				else
					print(i.." = "..tostring(v))
				end
			end
		end
		if not (value == nil and type_name == nil) then
			if value == nil then value = "nil" end
			if type_name == nil then type_name = "nil" end
			print("value: "..value..", type = "..type_name)
		end
	end
end

local function print_table2(t)
	if type(t) ~= "nil" then
		for i, v in pairs(t) do
			if type(v) == "table" then
				print_table(v)
			elseif type(v) == "nil" then
				print(i.." = nil")
			else
				print(i.." = "..tostring(v))
			end
		end
	end
end

function GcProtoBuf:Debug(str, ...)
	if not self.debugging then return end
	if not str or str:len() == 0 then return end
	if (...) then
		if str:find("%%[dfs%d%.]") then
			str = str:format(...)
		else
			str = strjoin(" ", str, tostringall(...))
		end
	end
	self.debugFrame:AddMessage("|cffff9933GcProtoBuf:|r " .. str)
end

function GcProtoBuf:new()
	local pb = { }
	self.__index = self
	setmetatable(pb, self)
	return pb
end

function GcProtoBuf:push(data_type, ...)
	local args = {...}
	local protobuf, value
	if (data_type >= PbType.bool) and (data_type <= PbType.double) then
		-- add integer values
		for _, v in my_ipairs(args) do
			if data_type == PbType.bool then
				-- boolean
				if type(v) == "boolean" then
					if v then value = true else value = false end
				elseif type(v) == "number" then
					if v ~= 0 then value = true else value = false end
				else
					value = false
				end
			elseif (data_type >= PbType.int8) and (data_type <= PbType.uint64) then
				-- signed or unsigned integer
				if type(v) == "number" then
					if (data_type >= PbType.int8) and (data_type <= PbType.int64) then
						-- signed integer [128, 32768, 2147483648 ...]
						local typeinfo = PbTypeInfo[data_type]
						if typeinfo then
							max_value = typeinfo.max_val
						else
							max_value = 2147483648
						end
						value = fmod(floor(v), max_value)
					elseif (data_type >= PbType.uint8) and (data_type <= PbType.uint64) then
						-- unsigned integer [256, 65536, 4294967296 ...]
						local max_value
						local typeinfo = PbTypeInfo[data_type]
						if typeinfo then
							max_value = typeinfo.max_val
						else
							max_value = 4294967296
						end
						value = fmod(floor(abs(v)), max_value)
					end
				elseif type(v) == "boolean" then
					if v then value = 1 else value = 0 end
				else
					value = 0
				end
			else
				-- float or double
				if type(v) == "number" then
					value = v
				else
					value = 0.0
				end
			end
			protobuf = {
				type = data_type,
				value = value
			}
			tinsert(self._entries, protobuf)
			self.core.length = self.core.length + 1
			self.core.data_size = self.core.data_size + PbTypeInfo[data_type].data_size
		end
	elseif data_type == PbType.string or data_type == PbType.ustring then
		-- add string values
		for _, v in my_ipairs(args) do
			if type(v) == "nil" then
				value = PB_NIL_STRING
			elseif type(v) == "number" or type(v) == "boolean" then
				value = tostring(v)
			elseif type(v) == "string" then
				value = v
			else
				value = PB_UNKNOWN_STRING
			end
			protobuf = {
				type = data_type,
				value = value
			}
			tinsert(self._entries, protobuf)
			self.core.length = self.core.length + 1
			self.core.data_size = self.core.data_size + strlen(value) + 1
		end
	elseif data_type == PbType.binary then
		-- add binary values
		for _, v in my_ipairs(args) do
			if v ~= nil then
				value = tostring(v)
				protobuf = {
					type = data_type,
					value = value
				}
				tinsert(self._entries, protobuf)
				self.core.length = self.core.length + 1
				self.core.data_size = self.core.data_size + floor((strlen(value) + 1) / 2) + 1
			end
		end
	elseif data_type == PbType.array then
		-- add array values
		local n = 1;
		local data_size
		local is_valid = false
		for _, v in my_ipairs(args) do
			if fmod(n, 2) == 1 then
				if type(v) == "number" then
					data_size = floor(tonumber(v))
					is_valid = true
				else
					is_valid = false
				end
			else
				if is_valid then
					protobuf = {
						type = data_type,
						value = v
					}
					tinsert(self._entries, protobuf)
					self.core.length = self.core.length + 1
					self.core.data_size = self.core.data_size + data_size
				end
			end
			n = n + 1
		end
	end
end

function GcProtoBuf:pop(num)
	-- get num values from stack
	local num = num or 1

	-- return table
	local entries = {}

	-- get values into entries
	for i = 1, num do
		-- get last entry
		if #self._entries ~= 0 then
			-- pop last value
			tinsert(entries, self._entries[#self._entries])
			-- remove last value
			tremove(self._entries)

			self.core.length = self.core.length - 1
		else
			break
		end
	end
	-- return unpacked entries
	return unpack(entries)
end

-- get entries
function GcProtoBuf:getn(num)
	return #self._entries
end

-- list values
function GcProtoBuf:list()
	if GcProtoBuf.debugging then
		print_table(self._entries)
	end
	--[[
	--print("GcProtoBuf:list()")
	for i, v in pairs(self._entries) do
		--print(i, v)
		self:Debug("Table: ", i, ", Value: ", v)
	end
	--]]
	self:Debug("GcProtoBuf:list()")
	self:Debug("GcProtoBuf.core.length", self.core.length)
	self:Debug("GcProtoBuf.core.data_size", self.core.data_size)
end
